<?
class ZPlus_Cache
{
  public function __construct(array $opts = array())
  {
    if (isset($opts['folder']))
      $target = $opts['folder'];
    else
      $target = BASE_DIR.'/cache';
    if (isset($opts['group']))
      $target .= '/'.$opts['group'];
    else
      $target .= '/default';
    $this->target = $target.'/';
    $this->tag = $target.'/tag/';
    $this->cache = array();
    $this->key_stack = array();
    $this->lock_map = array();
  }

  public function start($key, $echoData = true)
  {
    $val = $this->load($key);
    if ($val !== false)
    {
      if ($echoData)
      {
        echo $val;
        return true;
      }
      else
      {
        return $val;
      }
    }
    $this->key_stack[] = $key;
    ob_start();
    return false;
  }

  public function end($tag = null, $echoData = true)
  {
    $key = array_pop($this->key_stack);
    if ($key === null)
    {
      throw new Exception('end() without start()');
      return;
    }
    $val = ob_get_clean();
    if ($echoData) echo $val;
    $this->save($key, $val, $tag);
  }

  public function test($key)
  {
    if (isset($this->cache[$key])) return true;
    return file_exists($this->target.$key);
  }

  public function load($key)
  {
    if (isset($this->cache[$key])) return $this->cache[$key];
    if (!file_exists($this->target.$key)) return false;
    $val = self::_file_get_contents($this->target.$key);
    if ($val == null) return false;
    $this->cache[$key] = &$val;
    return $val;
  }

  public function save($key, $val, $tag = null)
  {
    $this->cache[$key] = &$val;
    if (!file_exists($this->target)) mkdir($this->target, 0777, true);
    self::_file_put_contents($this->target.$key, $val);

    if (!empty($tag))
    {
      if (!file_exists($this->tag)) mkdir($this->tag, 0777, true);
      $f = self::_lock_file($this->tag.$tag, '{}');
      $meta = json_decode(stream_get_contents($f));
      if (!isset($meta[$key]))
      {
        $meta[$key] = 1;
        fseek($f, 0);
        ftruncate($f, 0);
        fwrite($f, $json_encode($meta));
      }
      self::_unlock_file($f);
    }
  }

  public function remove($key)
  {
    if (file_exists($this->target.$key))
      unlink($this->target.$key);
  }

  public function removeTag($tag)
  {
    $f = self::_lock_file($this->tag.$tag, '{}');
    if ($f == false) return;
    $meta = json_decode(stream_get_contents($f));
    // remove all cache
    foreach ($meta as $k => $v)
      @unlink($this->target.$k);
    self::_unlock_file($f);
    unset($this->tag_cache[$tag]);
    @unlink($this->tag.$tag);
  }

  public static function _file_get_contents($name)
  {
    $r = false;
    $f = @fopen($name, 'rb');
    if ($f)
    {
      @flock($f, LOCK_SH);
      $r = stream_get_contents($f);
      @flock($f, LOCK_UN);
      @fclose($f);
    }
    return $r;
  }

  public static function _file_put_contents($name, $content)
  {
    $r = false;
    $f = @fopen($name, 'a+b');
    if ($f)
    {
      @flock($f, LOCK_EX);
      fseek($f, 0);
      ftruncate($f, 0);
      $t = @fwrite($f, $content);
      if ($t !== false) $r = true;
      @fclose($f);
    }
    return $r;
  }

  public function lock($key, $def_val)
  {
    if (isset($this->lock_map[$key]))
    {
      throw new Exception('double lock()');
    }
    if (!file_exists($this->target)) mkdir($this->target, 0777, true);
    $f = self::_lock_file($this->target.$key, $def_val);
    // open failed
    if ($f === false) return $def_val;
    $val = stream_get_contents($f);
    $this->lock_map[$key] = $f;
    return $val;
  }

  public function unlock($key, $new_val)
  {
    if (!isset($this->lock_map[$key]))
    {
      throw new Exception('not lock() yet');
    }
    $f = $this->lock_map[$key];
    unset($this->lock_map[$key]);
    fseek($f, 0);
    ftruncate($f, 0);
    fwrite($f, $new_val);
    self::_unlock_file($f);
  }

  public function discard($key)
  {
    if (!isset($this->lock_map[$key]))
    {
      throw new Exception('not lock() yet');
    }
    $f = $this->lock_map[$key];
    unset($this->lock_map[$key]);
    self::_unlock_file($f);
  }

  public static function _lock_file($name, $default)
  {
    $f = @fopen($name, 'r+b');
    if (!$f)
    {
      $f = @fopen($name, 'wb');
      if (!$f) return false;
      if (!flock($f, LOCK_EX) || @fwrite($f, $default) === false)
      {
        @fclose($f);
        @unlink($name);
        return false;
      }
      fseek($f, 0);
    }
    else
    {
      if (!flock($f, LOCK_EX))
      {
        @fclose($f);
        return false;
      }
    }
    return $f;
  }

  public static function _unlock_file($f)
  {
    flock($f, LOCK_UN);
    @fclose($f);
  }
}
